using System;
using Moq;
using NUnit.Framework;
using Tycho.Grammar;
using Tycho.Parser.Peg;
using Tycho.Parser.Tests.Peg;

namespace Tycho.UnitTests.Grammar {
    [TestFixture]
    public class GIVEN_RuleCompiler {
        [Test]
        public void WHEN_passed_choice_THEN_compiler_should_compile_choice_rule () {
            var choice = CompileRule ("'a' / 'b' / 'c'");
            
            AssertParsed (choice, "a ", "a", "b", "c");
            AssertNotParsed (choice, "d");
        }

        [Test]
        public void WHEN_passed_sequence_THEN_compiler_should_compile_sequence_rule () {
            var choice = CompileRule ("'a' 'b' 'c'");

            AssertParsed (choice, "a b c");
            AssertNotParsed (choice, "b c a", "a b", "d e f");
        }

        [Test]
        public void WHEN_passed_optional_rule_THEN_compiler_should_compile_optional_rule () {
            var choice = CompileRule ("'a'?");

            AssertParsed (choice, "a", "");
            AssertNotParsed (choice, "b", "a b");
        }

        [Test]
        public void WHEN_passed_zero_or_more_THEN_compiler_should_compile_zero_or_more_rule () {
            var choice = CompileRule ("'a'*");

            AssertParsed (choice, "a", "a a", "", "a a a");
            AssertNotParsed (choice, "b", "a b", "a +");
        }

        [Test]
        public void WHEN_passed_one_or_more_THEN_compiler_should_compile_one_or_more_rule () {
            var choice = CompileRule ("'a'+");

            AssertParsed (choice, "a", "a a", "a a a");
            AssertNotParsed (choice, "b", "a b", "a +", "");
        }

        [Test]
        public void WHEN_passed_subrule_THEN_compiler_returns_subrule () {
            var choice = CompileRule ("('a' 'b')");

            AssertParsed (choice, "a b");
            AssertNotParsed (choice, "b", "a", "a +", "");
        }

        [Test]
        public void WHEN_passed_delimited_THEN_compiler_should_compile_delimited_rule () {
            var choice = CompileRule ("'a' ',' ...");

            AssertParsed (choice, "a, a, a", "a", "a,", "a, a", "");
            AssertNotParsed (choice, "b", "a +", ", a");
        }

        [Test]
        public void WHEN_passed_production_lookup_THEN_that_production_should_be_used_to_parse_source () {
            IProductionCompilerEnvironment environment = CreateEnvironmentWithIdentifierAsDefault ();
            var lookup = new ProductionLookup ();
            lookup.Add ("a", IdentifierProduction.CreateTerminal ());
            lookup.Add ("b", IntegerProduction.CreateTerminal ());

            var choice = CompileRule ("a b", environment, lookup);

            AssertParsed (choice, "a 8", "b 9", "c 1", " d    45", "  d   0");
            AssertNotParsed (choice, "a +", ", a", "9 a", "a a", "9 9");
        }

        [Test]
        public void WHEN_passed_capture_with_production_THEN_compiler_should_return_capture_with_that_production () {
            var evaluator = new IdentifierProductionCompilerEvaluator (null);
            var lookup = new ProductionLookup();
            lookup.Add ("id", IdentifierProduction.CreateTerminal ());

            var choice = CompileRule ("a: id", evaluator, lookup);

            AssertCaptured (choice, "b", "a", "b");
            AssertCaptured (choice, " b", "a", "b");
            AssertNotParsed (choice, "a +", ", a");
        }

        [Test]
        public void WHEN_passed_more_than_one_capture_THEN_compiler_should_make_that_capture_a_multiple () {
            IProductionCompilerEnvironment environment = CreateEnvironmentWithIdentifierAsDefault ();
            var lookup = new ProductionLookup();
            lookup.Add ("id", IdentifierProduction.CreateTerminal ());
            lookup.Add ("int", IntegerProduction.CreateTerminal ());

            var withMultiples = CompileRule ("a: id ',' b: id ',' a: int", environment, lookup);
            ITerm term = withMultiples.ParseTerm ("x, y, 3");
            term.ShouldBe ("rule {a: [x, 3], b: y}");
        }

        private IProductionCompilerEnvironment CreateEnvironmentWithIdentifierAsDefault () {
            var evaluator = new Mock<IProductionCompilerEnvironment> ();

            var identifier = IdentifierProduction.CreateTerminal ();
            evaluator.SetupGet (l => l.Default).Returns (identifier);
            return evaluator.Object;
        }

        private static void AssertCaptured (IProduction production, string source, string captureName, string captureIdentifier) {
            ITerm term = production.ParseTerm (source);
            Assert.IsNotNull (term);
            var compositeTerm = term as CompositeTerm;
            Assert.IsNotNull (compositeTerm);
            Assert.AreEqual ("rule", compositeTerm.Name);
            Assert.AreEqual (captureIdentifier, ((IdentifierTerm) compositeTerm.SubTerms[captureName]).Name);
        }

        private static void AssertParsed (IProduction production, params string [] sources) {
            foreach (var source in sources) {
                Assert.IsNotNull (production.ParseTerm (source), "expected `" + source + "' to parse");
            }
        }

        private static void AssertNotParsed (IProduction production, params string [] sources) {
            foreach (var source in sources) {
                production.AssertNotParsed (source);
            }
        }

        private static IProduction CompileRule (string grammarSource) {
            return CompileRule (grammarSource, null, null);
        }

        static IProduction CompileRule (string grammarSource, IProductionCompilerEnvironment environment, IProductionLookup lookup) {
            IProduction syntax = GrammarLoader.CreateSyntax (IdentifierProduction.CreateTerminal ());
            ITerm syntaxTerm = syntax.ParseTerm (grammarSource);

            var ruleCompiler = new RuleCompiler (new GrammarCompiler ());
            return CompileRule (ruleCompiler, syntaxTerm, environment, lookup ?? new Mock<IProductionLookup> ().Object);
        }

        private static IProduction CompileRule (IRuleCompiler ruleCompiler, ITerm syntaxTerm, IProductionCompilerEnvironment environment, IProductionLookup lookup) {
            var captureCounter = new CaptureCounter ();

            var context = new ProductionCompilerContext (
                environment,
                lookup,
                new Mock<IDynamicGrammar> ().Object,
                new Mock<IDeferredCompilations> ().Object);

            IRule rule = ruleCompiler.Compile (
                syntaxTerm,
                context,
                captureCounter);

            return new NonTerminal ("rule", rule, captureCounter.MultipleCaptures);
        }
    }
}